F# meets the Raspberry Pi
I got a Pi for my birthday! A great excuse to get back into electronics.
After unsuccessfully struggling to get the F# compiler to build under the stable version of mono for Debian Wheezy, I realised that F# programs work just fine if you build normally from a windows computer, throw in FSharp.Core.dll in the /bin/ and copy it over. So I have a setup now where I work with VS2012 / Sublime and sync the executable and libraries with WinScp (or indeed the Scp plugin for FAR Manager).
Next up is to get access to the hardware. I built this C library as a shared object, dumped it in with the other binaries of my project and it worked through P/Invoke with no hassle at all !
I've quite a bit of electronics experience, so after I did some basic tests that the various I/O pins could be set to output and switch from High to Low, I skipped the traditional "hello world" of hardware (blinking an LED) and figured I'd try something a little bit more ambitious. I have a little LCD screen laying around in my old gear, it's a standard LCD driven by the Hitachi HD4480 LCD Controller. You can use these in 8-bit or 4-bit mode, with the 8 bit mode needing 4 more I/O pins. I'm using the 4 bit mode because I don't really have that many pins and it's pretty easy (although a little fiddly) to use it in 4-bit mode.
I'm using a total of 7 I/O pins, E (enable), RS (switch between command and character mode), RW (I'm not actually using this right now) , and then DB4 through DB7 which are the 4 input bits (they are the higher nibble of the full 8 bits). This is the circuit :
In this schematic the Pi pins relate to the actual physical pin numbers, however in the code a mapping is needed over to what the Pi internally calls its GPIO pins. I created an enum for this purposes, only containing the pins I am using for now
1 2 3 4 5 6 7 8 |
type GPIOPins = | Pin_11 = 17u | Pin_12 = 18u | Pin_13 = 27u | Pin_15 = 22u | Pin_16 = 23u | Pin_18 = 24u | Pin_22 = 25u |
For example the physical pin 11 maps to GPIO number 17. Infact when I first hooked this circuit up and wrote all the code to perform the LCD initilization I couldn't get it to work. Thankfully I happen to have a 16 channel logic analyzer in my scope so I hooked up all the inputs, set it to a single sweep triggering on the rising edge of the Enable pin over 500ms and noticed that the RW pin was always high - strange (I neglected to take a picture of the waveforms for this post :( ). Turns out that the Pi user manual is WRONG, I have got a slightly later revision of the board where pin 13 is mapped to 27, not 21!
The next bit of code imports a couple of the functions from the C library and creates a couple of mini functions around them
1 2 3 4 5 6 7 8 9 10 11 12 |
[<DllImportAttribute("libbcm2835.so", EntryPoint = "bcm2835_init")>] extern bool bcm2835_init() [<DllImport("libbcm2835.so", EntryPoint = "bcm2835_gpio_fsel")>] extern void bcm2835_gpio_fsel(GPIOPins pin, bool mode_out); [<DllImport("libbcm2835.so", EntryPoint = "bcm2835_gpio_write")>] extern void bcm2835_gpio_write(GPIOPins pin, bool value); let fsel pin value = bcm2835_gpio_fsel(pin,value) let write pin value = bcm2835_gpio_write(pin,value) let wait (ms:int) = System.Threading.Thread.Sleep(ms) |
To use the LCD you write some bits to the data pins, then bring the Enable pin high for a few us then pull it low again ("pulse"). The LCD then does something depending on the input bits. In order to prepare it for 4-bit use I first have to send it a few 0x03 (0011) packets as per the spec indicates. Then I can switch it into 4-bit mode (0x2). From this point on, I can use all the 8-bit commands from the spec. Because I'm running in 4 bit mode I have to send the high nibble first, pulse, then send the low nibble. I wrapped some of the LCD functionality up in a F# record type (note: all this code is just a first stab, everything with hardware is inherently to do with mutating state so I won't be using a lot of the real functional features of the language just yet, but I'll see what I can do about that later)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
type LCDCommands = | AllLow = 0b00000000 | Clear = 0b00000001 | Home = 0b00000010 | FourBit = 0b00100000 | TwoLine = 0b00001100 | DisplayOn = 0b00001100 | CursorOn = 0b00000001 | AutoIncCursor = 0b00000110 | Line2 = 0xC0 type LCD = { E : GPIOPins; RW : GPIOPins; RS : GPIOPins; DB4 : GPIOPins; DB5 : GPIOPins; DB6 : GPIOPins; DB7 : GPIOPins; } with member lcd.Pulse() = // toggles enable write lcd.E true; wait 1 write lcd.E false; wait 1 member lcd.WriteNibble(value) = // write the lower four bits to the data pins and pulses write lcd.DB7 (value > 3 &&& 0x1 = 0x1) write lcd.DB6 (value > 2 &&& 0x1 = 0x1) write lcd.DB5 (value > 1 &&& 0x1 = 0x1) write lcd.DB4 (value &&& 0x1 = 0x1) lcd.Pulse() wait 1 member lcd.WriteByte(value) = lcd.WriteNibble(value > 4) // write high nibble first lcd.WriteNibble(value) member lcd.Command = int > lcd.WriteByte |
I have captured some of the LCD commands in another enum - some of these have to be OR'd together as per the spec. I've just encoded the ones I'm going to use. The there's the pulse which toggles enable with a tiny delay. Because I'm in 4 bit mode I'll always be writing nibbles with a pulse at the end - the WriteByte function simply writes the high nibble first then the low nibble as the spec indicates. The last function is just a wrapper so I can directly use the LCDCommand enum.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
member lcd.Initialize() = // I am only using the (annoyingly fiddly) 4 bit mode // assume 1000ms or so has passed since program start up // make sure pins are set to output fsel lcd.E true; fsel lcd.RW true fsel lcd.RS true; fsel lcd.DB4 true fsel lcd.DB5 true; fsel lcd.DB6 true fsel lcd.DB7 true // zero them all out lcd.Command LCDCommands.AllLow // to start with we are only writing special wakeup nibbles lcd.WriteNibble(0x3); wait 5 // as per spec, first call has a 5ms wait lcd.WriteNibble(0x3); wait 1 lcd.WriteNibble(0x3); wait 1 // now set into 4 bit mode and send 8 bits in 2 nibbles from now on lcd.WriteNibble(0x2) lcd.Command(LCDCommands.FourBit ||| LCDCommands.TwoLine) // set 5x8 mode 2 lines lcd.Command(LCDCommands.DisplayOn ||| LCDCommands.CursorOn) // switch it on lcd.Command(LCDCommands.AutoIncCursor) |
This is the startup sequence - set all the pins to Output, zero them all out, and then follow the startup sequence as per the spec. initially I have to just use nibbles, until the wake-up sequence is complete, then I can set it to 4-bit mode and use full byte commands. Once the display is in 4-bit mode I switch it to 5x8 mode with 2 lines and switch the screen on with a flashing cursor and so on.
1 2 3 4 5 |
member lcd.WriteText(text:string,clear) = if clear then lcd.Command(LCDCommands.Clear) write lcd.RS true; wait 1 Encoding.ASCII.GetBytes(text) |> Seq.iter(int > lcd.WriteByte) write lcd.RS false; wait 1 |
Lastly a function to output some text. To do this you have to set the LCD into character output mode by pulling RS high; then you can send ASCII codes and the LCD will print them.
Pulling this all together I wrote that classic silly number-guessing game you write when learning to program, with the output on the LCD:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
[<EntryPoint>] let main argv = try match bcm2835_init() with | true -> let lcd = { E = GPIOPins.Pin_11; RW = GPIOPins.Pin_12; RS = GPIOPins.Pin_13; DB4 = GPIOPins.Pin_15; DB5 = GPIOPins.Pin_16; DB6 = GPIOPins.Pin_18; DB7 = GPIOPins.Pin_22 } wait 1000 lcd.Initialize() let rec loop number attempts = try let guess = Console.ReadLine() |> Int32.Parse if guess = number then lcd.WriteText("CORRECT!!",true) lcd.WriteByte(0xC0); lcd.WriteText("YOU WIN!",false) elif attempts + 1 > 5 then lcd.WriteText("WRONG!!",true) lcd.WriteByte(0xC0); lcd.WriteText("YOU LOSE!!",false) else lcd.WriteText("WRONG!! ",true) lcd.WriteText((if number < guess then "< " else "> ") + guess.ToString(),false) lcd.WriteByte(0xC0); wait 2 lcd.WriteText("GUESS AGAIN!",false) loop number (attempts + 1 ) with | _ -> printfn "Number not reconigsed. Try again" loop number attempts lcd.WriteText("Guess a number",true) lcd.WriteByte(0xC0); wait 2 lcd.WriteText("0 <---> 50",false) loop (System.Random(DateTime.Now.Millisecond).Next(51)) 0 | false -> printfn "failed to init" with | ex -> printfn "exception thrown : %s" <| ex.ToString() Console.Read() |
Here's a pic of it working ..
Cool! This was just a silly project to test everything is working properly - I can take over the world now.